package com.tonyodev.fetch2.downloader;

import com.tonyodev.fetch2.Download;
import com.tonyodev.fetch2.EnqueueAction;
import com.tonyodev.fetch2.Error;
import com.tonyodev.fetch2.FetchErrorUtils;
import com.tonyodev.fetch2.database.DownloadInfo;
import com.tonyodev.fetch2.exception.FetchException;
import com.tonyodev.fetch2.helper.FileDownloaderDelegate;
import com.tonyodev.fetch2.provider.NetworkInfoProvider;
import com.tonyodev.fetch2.util.Defaults;
import com.tonyodev.fetch2.util.TypeConverterExtensions;
import com.tonyodev.fetch2core.AverageCalculator;
import com.tonyodev.fetch2core.DownloadBlock;
import com.tonyodev.fetch2core.DownloadBlockInfo;
import com.tonyodev.fetch2core.Downloader;
import com.tonyodev.fetch2core.FetchCoreUtils;
import com.tonyodev.fetch2core.FetchErrorStrings;
import com.tonyodev.fetch2core.FileSlice;
import com.tonyodev.fetch2core.InterruptMonitor;
import com.tonyodev.fetch2core.Logger;
import com.tonyodev.fetch2core.OutputResourceWrapper;
import com.tonyodev.fetch2core.StorageResolver;

import org.jetbrains.annotations.NotNull;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;

import timber.log.Timber;

/**
 * @since 2021-05-20
 */
public class SequentialFileDownloaderImpl implements FileDownloader {

    private Download initialDownload;
    private Downloader downloader;
    private long progressReportingIntervalMillis;
    private Logger logger;
    private NetworkInfoProvider networkInfoProvider;
    private boolean retryOnNetworkGain;
    private boolean hashCheckingEnabled;
    private StorageResolver storageResolver;
    private boolean preAllocateFileOnCreation;

    private boolean interrupted = false;
    private boolean terminated = false;
    private FileDownloader.Delegate delegate = null;
    private DownloadInfo downloadInfo;
    private Download download;
    private long downloaded = 0L;
    private long total = -1L;
    private boolean totalUnknown = false;
    private double averageDownloadedBytesPerSecond = 0.0f;
    private AverageCalculator movingAverageCalculator;
    private long estimatedTimeRemainingInMilliseconds = -1;
    private ExecutorService executorService = null;
    private int actionsCounter = 0;
    private int actionsTotal = 0;
    private final Object lock = new Object();
    private Throwable throwable = null;
    private List<FileSlice> fileSlices = new ArrayList<>();
    private OutputResourceWrapper outputResourceWrapper = null;
    private DownloadBlockInfo downloadBlock;
    private int totalDownloadBlocks = 1;


    public SequentialFileDownloaderImpl(Download initialDownload,
                                        Downloader downloader,
                                        long progressReportingIntervalMillis,
                                        Logger logger,
                                        NetworkInfoProvider networkInfoProvider,
                                        boolean retryOnNetworkGain,
                                        boolean hashCheckingEnabled,
                                        StorageResolver storageResolver,
                                        boolean preAllocateFileOnCreation) {

        this.initialDownload = initialDownload;
        this.downloader = downloader;
        this.progressReportingIntervalMillis = progressReportingIntervalMillis;
        this.logger = logger;
        this.networkInfoProvider = networkInfoProvider;
        this.retryOnNetworkGain = retryOnNetworkGain;
        this.hashCheckingEnabled = hashCheckingEnabled;
        this.storageResolver = storageResolver;
        this.preAllocateFileOnCreation = preAllocateFileOnCreation;

        downloadBlock = new DownloadBlockInfo();
        downloadBlock.setBlockPosition(1);
        downloadBlock.setDownloadId(initialDownload.getId());

        download = getDownloadInfo();
    }

    @Override
    public boolean getInterrupted() {
        return interrupted;
    }

    @Override
    public void setInterrupted(boolean interrupted) {
        this.interrupted = interrupted;
        if (null != delegate && delegate instanceof FileDownloaderDelegate) {
            ((FileDownloaderDelegate) delegate).setInterrupted(interrupted);
        }
    }

    @Override
    public boolean getTerminated() {
        return terminated;
    }

    @Override
    public void setTerminated(boolean terminated) {
        this.terminated = terminated;
        if (null != delegate && delegate instanceof FileDownloaderDelegate) {
            ((FileDownloaderDelegate) delegate).setInterrupted(terminated);
        }
    }

    @Override
    public boolean getCompletedDownload() {
        return isDownloadComplete();
    }

    @Override
    public Delegate getDelegate() {
        return delegate;
    }

    @Override
    public void setDelegate(Delegate delegate) {
        this.delegate = delegate;
    }

    private DownloadInfo getDownloadInfo() {
        if (downloadInfo == null && delegate != null) {
            downloadInfo = TypeConverterExtensions.toDownloadInfo(initialDownload, delegate.getNewDownloadInfoInstance());
        }
        return downloadInfo;
    }


    @Override
    public Download getDownload() {
        getDownloadInfo();
        getDownloadInfo().setDownloaded(downloaded);
        getDownloadInfo().setTotal(total);
        return getDownloadInfo();
    }

    @Override
    public void run() {
        OutputResourceWrapper outputResourceWrapper = null;
        BufferedInputStream input = null;
        Downloader.Response response = null;
        try {
            downloaded = initialDownload.getDownloaded();
            total = initialDownload.getTotal();
            getDownloadInfo().setDownloaded(downloaded);
            getDownloadInfo().setTotal(total);
            if (!interrupted && !terminated) {
                Downloader.ServerRequest request = getRequest();
                response = downloader.execute(request, interruptMonitor);
                boolean isResponseSuccessful;
                if (response != null) {
                    setIsTotalUnknown(response);
                    isResponseSuccessful = response.isSuccessful();
                } else {
                    isResponseSuccessful = false;
                }
                if (!interrupted && !terminated && response != null && isResponseSuccessful) {
                    if (response.getCode() == HttpURLConnection.HTTP_PARTIAL || response.getAcceptsRanges()) {
                        downloaded = initialDownload.getDownloaded();
                    } else {
                        downloaded = 0;
                    }
                    total = response.getContentLength();
                    long seekPosition;
                    if (response.getCode() == HttpURLConnection.HTTP_PARTIAL) {
                        logger.d("FileDownloader resuming Download " + download);
                        seekPosition = downloaded;
                    } else {
                        logger.d("FileDownloader starting Download " + download);
                        seekPosition = 0L;
                    }
                    getDownloadInfo().setDownloaded(downloaded);
                    getDownloadInfo().setTotal(total);
                    if (!storageResolver.fileExists(request.getFile())) {
                        storageResolver.createFile(request.getFile(), initialDownload.getEnqueueAction() == EnqueueAction.INCREMENT_FILE_NAME);
                    }
                    if (preAllocateFileOnCreation) {
                        try {
                            storageResolver.preAllocateFile(request.getFile(), getDownloadInfo().getTotal());
                            outputResourceWrapper = storageResolver.getRequestOutputResourceWrapper(request);
                            outputResourceWrapper.setWriteOffset(seekPosition);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                    if (!interrupted && !terminated) {
                        int bufferSize = downloader.getRequestBufferSize(request);
                        input = new BufferedInputStream(response.getByteStream(), bufferSize);
                        getDownloadInfo().setDownloaded(downloaded);
                        getDownloadInfo().setTotal(total);
                        downloadBlock.setDownloadedBytes(downloaded);
                        downloadBlock.setStartByte(seekPosition);
                        downloadBlock.setEndByte(total);
                        if (!terminated && !interrupted) {
                            getDownloadInfo().setEtaInMilliSeconds(-1);
                            getDownloadInfo().setDownloadedBytesPerSecond(-1);
                            if (null != delegate) {
                                List<DownloadBlock> downloadBlocklist = new ArrayList<>();
                                downloadBlocklist.add(downloadBlock);
                                delegate.onStarted(getDownloadInfo(), downloadBlocklist, totalDownloadBlocks);
                                delegate.onDownloadBlockUpdated(getDownloadInfo(), downloadBlock, totalDownloadBlocks);
                            }

                        }
                        writeToOutput(input, outputResourceWrapper, bufferSize);
                    }
                } else if (response == null && !interrupted && !terminated && !isDownloadComplete()) {
                    throw new FetchException(FetchErrorStrings.EMPTY_RESPONSE_BODY);
                } else if (!isResponseSuccessful && !interrupted && !isDownloadComplete()) {
                    throw new FetchException(FetchErrorStrings.RESPONSE_NOT_SUCCESSFUL);
                } else if (!interrupted && !terminated && downloaded < total && !isDownloadComplete()) {
                    throw new FetchException(FetchErrorStrings.UNKNOWN_ERROR);
                }
            }
            if (!isDownloadComplete() && !terminated && !interrupted) {
                getDownloadInfo().setDownloaded(downloaded);
                getDownloadInfo().setTotal(total);
                downloadBlock.setDownloadedBytes(downloaded);
                downloadBlock.setEndByte(total);
                if (!terminated && !interrupted) {
                    if (null != delegate) {
                        delegate.saveDownloadProgress(getDownloadInfo());
                        delegate.onDownloadBlockUpdated(getDownloadInfo(), downloadBlock, totalDownloadBlocks);
                    }
                    getDownloadInfo().setEtaInMilliSeconds(estimatedTimeRemainingInMilliseconds);
                    getDownloadInfo().setDownloadedBytesPerSecond(getAverageDownloadedBytesPerSecond());
                    if (null != delegate) {
                        delegate.onProgress(getDownloadInfo(), getDownloadInfo().getEtaInMilliSeconds(),
                                getDownloadInfo().getDownloadedBytesPerSecond());
                    }
                }
            } else if (isDownloadComplete() && response != null) {
                verifyDownloadCompletion(response);
            }
        } catch (Exception e) {
            if (!interrupted && !terminated) {
                logger.e("FileDownloader download:" + e);
                Timber.e("FileDownloader download:" + e);

                Error error = FetchErrorUtils.getErrorFromThrowable(e);
                error.setThrowable(e);

                if (response != null) {
                    error.setHttpResponse(FetchCoreUtils.copyDownloadResponseNoStream(response));
                }
                if (retryOnNetworkGain) {
                    boolean disconnectDetected = !networkInfoProvider.isNetworkAvailable();
                    for (int i = 0; i < 10; i++) {
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e2) {
                            logger.e("FileDownloader", e2);
                            break;
                        }
                        if (!networkInfoProvider.isNetworkAvailable()) {
                            disconnectDetected = true;
                            break;
                        }
                    }
                    if (disconnectDetected) {
                        error = Error.NO_NETWORK_CONNECTION;
                    }
                }
                getDownloadInfo().setDownloaded(downloaded);
                getDownloadInfo().setTotal(total);
                getDownloadInfo().setError(error);
                downloadBlock.setDownloadedBytes(downloaded);
                downloadBlock.setEndByte(total);
                if (!terminated && !interrupted) {
                    getDownloadInfo().setEtaInMilliSeconds(-1);
                    getDownloadInfo().setDownloadedBytesPerSecond(-1);
                    if (null != delegate) {
                        delegate.onError(getDownloadInfo(), error, e);
                    }
                }
            }
        } finally {
            try {
                if (null != input) {
                    input.close();
                }
            } catch (Exception e) {
                logger.e("FileDownloader", e);
            }
            if (response != null) {
                try {
                    downloader.disconnect(response);
                } catch (Exception e) {
                    logger.e("FileDownloader", e);
                }
            }
            try {
                if (null != outputResourceWrapper) {
                    outputResourceWrapper.close();
                }
            } catch (Exception e) {
                logger.e("FileDownloader", e);
            }
            terminated = true;
        }
    }

    private boolean isDownloadComplete() {
        return ((downloaded > 0 && total > 0) || totalUnknown) && (downloaded >= total);
    }

    private void setIsTotalUnknown(Downloader.Response response) {
        if (response.isSuccessful() && response.getContentLength() == -1L) {
            totalUnknown = true;
        }
    }

    private void writeToOutput(BufferedInputStream input,
                               OutputResourceWrapper outputResourceWrapper,
                               int bufferSize) {
        long reportingStopTime;
        long downloadSpeedStopTime;
        long downloadedBytesPerSecond = downloaded;

        byte[] buffer = new byte[bufferSize];
        long reportingStartTime = System.nanoTime();
        long downloadSpeedStartTime = System.nanoTime();
        int read = 0;
        try {
            read = input.read(buffer, 0, bufferSize);
        } catch (IOException e) {
            e.printStackTrace();
        }
        while (!interrupted && !terminated && read != -1) {
            if (null != outputResourceWrapper) {
                try {
                    outputResourceWrapper.write(buffer, 0, read);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (!terminated && !interrupted) {
                downloaded += read;
                getDownloadInfo().setDownloaded(downloaded);
                getDownloadInfo().setTotal(total);
                downloadBlock.setDownloadedBytes(downloaded);
                downloadBlock.setEndByte(total);
                downloadSpeedStopTime = System.nanoTime();
                boolean downloadSpeedCheckTimeElapsed = FetchCoreUtils.hasIntervalTimeElapsed(downloadSpeedStartTime,
                        downloadSpeedStopTime, Defaults.DEFAULT_DOWNLOAD_SPEED_REPORTING_INTERVAL_IN_MILLISECONDS);

                if (downloadSpeedCheckTimeElapsed) {
                    downloadedBytesPerSecond = downloaded - downloadedBytesPerSecond;
                    movingAverageCalculator.add((double) downloadedBytesPerSecond);

                    averageDownloadedBytesPerSecond =
                            movingAverageCalculator.getMovingAverageWithWeightOnRecentValues();
                    estimatedTimeRemainingInMilliseconds = FetchCoreUtils.calculateEstimatedTimeRemainingInMilliseconds(
                            downloaded,
                            total,
                            getAverageDownloadedBytesPerSecond());
                    downloadedBytesPerSecond = downloaded;
                }

                reportingStopTime = System.nanoTime();
                boolean hasReportingTimeElapsed = FetchCoreUtils.hasIntervalTimeElapsed(reportingStartTime,
                        reportingStopTime, progressReportingIntervalMillis);


                if (hasReportingTimeElapsed) {
                    synchronized (lock) {
                        if (!interrupted && !terminated) {
                            getDownloadInfo().setDownloaded(downloaded);
                            getDownloadInfo().setTotal(total);
                            if (null != delegate) {
                                delegate.saveDownloadProgress(getDownloadInfo());
                            }
                            getDownloadInfo().setEtaInMilliSeconds(estimatedTimeRemainingInMilliseconds);
                            getDownloadInfo().setDownloadedBytesPerSecond(getAverageDownloadedBytesPerSecond());
                            if (null != delegate) {
                                delegate.onProgress(getDownloadInfo(), getDownloadInfo().getEtaInMilliSeconds(),
                                        getDownloadInfo().getDownloadedBytesPerSecond());
                            }
                        }
                    }
                    reportingStartTime = System.nanoTime();
                }

                if (hasReportingTimeElapsed) {
                    downloadBlock.setDownloadedBytes(downloaded);
                    if (!terminated && !interrupted) {
                        if (null != delegate) {
                            delegate.saveDownloadProgress(getDownloadInfo());
                            delegate.onDownloadBlockUpdated(getDownloadInfo(), downloadBlock, totalDownloadBlocks);
                        }
                        getDownloadInfo().setEtaInMilliSeconds(estimatedTimeRemainingInMilliseconds);
                        getDownloadInfo().setDownloadedBytesPerSecond(getAverageDownloadedBytesPerSecond());
                        if (null != delegate) {
                            delegate.onProgress(getDownloadInfo(), getDownloadInfo().getEtaInMilliSeconds(),
                                    getDownloadInfo().getDownloadedBytesPerSecond());
                        }
                    }
                    reportingStartTime = System.nanoTime();
                }
                if (downloadSpeedCheckTimeElapsed) {
                    downloadSpeedStartTime = System.nanoTime();
                }
                try {
                    read = input.read(buffer, 0, bufferSize);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        if (null != outputResourceWrapper) {
            try {
                outputResourceWrapper.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void verifyDownloadCompletion(Downloader.Response response) {
        if (!interrupted && !terminated && isDownloadComplete()) {
            total = downloaded;

            getDownloadInfo().setDownloaded(downloaded);
            getDownloadInfo().setTotal(total);


            downloadBlock.setDownloadedBytes(downloaded);
            downloadBlock.setEndByte(total);
            if (hashCheckingEnabled) {
                if (downloader.verifyContentHash(response.getRequest(), response.getHash())) {
                    if (!terminated && !interrupted) {

                        if (null != delegate) {
                            delegate.saveDownloadProgress(getDownloadInfo());
                            delegate.onDownloadBlockUpdated(getDownloadInfo(), downloadBlock, totalDownloadBlocks);
                        }
                        getDownloadInfo().setEtaInMilliSeconds(estimatedTimeRemainingInMilliseconds);
                        getDownloadInfo().setDownloadedBytesPerSecond(getAverageDownloadedBytesPerSecond());
                        Download completedDownload = getDownloadInfo().copy();
                        if (null != delegate) {
                            delegate.onProgress(getDownloadInfo(), getDownloadInfo().getEtaInMilliSeconds(),
                                    getDownloadInfo().getDownloadedBytesPerSecond());
                        }
                        getDownloadInfo().setEtaInMilliSeconds(-1);
                        getDownloadInfo().setDownloadedBytesPerSecond(-1);
                        if (null != delegate) {
                            delegate.onComplete(completedDownload);
                        }

                    }
                } else {
                    throw new FetchException(FetchErrorStrings.INVALID_CONTENT_HASH);
                }
            } else {
                if (!terminated && !interrupted) {
                    if (null != delegate) {
                        delegate.saveDownloadProgress(getDownloadInfo());
                        delegate.onDownloadBlockUpdated(getDownloadInfo(), downloadBlock, totalDownloadBlocks);
                    }
                    getDownloadInfo().setEtaInMilliSeconds(estimatedTimeRemainingInMilliseconds);
                    getDownloadInfo().setDownloadedBytesPerSecond(getAverageDownloadedBytesPerSecond());
                    Download completedDownload = getDownloadInfo().copy();
                    if (null != delegate) {
                        delegate.onProgress(getDownloadInfo(), getDownloadInfo().getEtaInMilliSeconds(),
                                getDownloadInfo().getDownloadedBytesPerSecond());
                    }
                    getDownloadInfo().setEtaInMilliSeconds(-1);
                    getDownloadInfo().setDownloadedBytesPerSecond(-1);
                    if (null != delegate) {
                        delegate.onComplete(completedDownload);
                    }
                }
            }
        }
    }

    private Downloader.ServerRequest getRequest() {
        Map headers = initialDownload.getHeaders();
        headers.put("Range", "bytes=" + downloaded + "-");
        return new Downloader.ServerRequest(
                initialDownload.getId(),
                initialDownload.getUrl(),
                headers,
                initialDownload.getFile(),
                FetchCoreUtils.getFileUri(initialDownload.getFile()),
                initialDownload.getTag(),
                initialDownload.getIdentifier(),
                FetchCoreUtils.GET_REQUEST_METHOD,
                initialDownload.getExtras(),
                false,
                "",
                1);
    }

    private long getAverageDownloadedBytesPerSecond() {
        if (averageDownloadedBytesPerSecond < 1) {
            return 0L;
        }
        return (long) Math.ceil(averageDownloadedBytesPerSecond);
    }

    private InterruptMonitor interruptMonitor = new InterruptMonitor() {

        @Override
        public boolean isInterrupted() {
            return interrupted;
        }
    };

}
